"""\
AmazonECS - query the Amazon E-Commerce Services API

David Janes
October 2008

- This is iterator/generator based: you need not worry about pages
- Style of argument names is non-pythonic to match Amazon's conventions.
- Props to Mark Pilgrim, Michael Josephson for 'pyamazon', which inspired writing this.

License:

	New BSD License

Example:

	a = ecs.AmazonECS(AccessKeyID = 'YOUR_AMAZON_ECS_LICENSE_KEY')
	for d in a.iterMusicByArtist('U2'):
		pprint.pprint(d)

History:

	- Version 0.1 2008-10-19
"""

import os
import sys
import urllib
import types
import xml.etree.ElementTree as ElementTree

class AmazonECSError(Exception): pass
class NoLicenseKeyError(AmazonECSError): pass
class BadLocaleError(AmazonECSError): pass
class NetworkError(AmazonECSError): pass
class BadResponseError(AmazonECSError): pass

Locale2api_urid = {
	"CA" : "http://ecs.amazonaws.ca/onca/xml?Service=AWSECommerceService",
	"DE" : "http://ecs.amazonaws.de/onca/xml?Service=AWSECommerceService",
	"FR" : "http://ecs.amazonaws.fr/onca/xml?Service=AWSECommerceService",
	"JP" : "http://ecs.amazonaws.jp/onca/xml?Service=AWSECommerceService",
	"UK" : "http://ecs.amazonaws.co.uk/onca/xml?Service=AWSECommerceService",
	"US" : "http://ecs.amazonaws.com/onca/xml?Service=AWSECommerceService",
}

def scrub_tag(tag):
	bracketx = tag.rfind('}')
	if bracketx > -1:
		tag = tag[bracketx + 1:]

	return	tag

node_ignored = {
	"BrowseNodes" : 1,
}

node_listd = {
	"Tracks.Disc.Track" : 1,
	"ItemAttributes.Author" : 1,
}

node_flattend = {
	"ItemAttributes" : 1,
	"ImageSets.ImageSet" : 1,
	"ImageSets" : 1,
	"Tracks" : 1,
	"Tracks.Disc" : 1,
	"Tracks.Disc.Track" : 1,
}

def node2d_raw(node0, path = None, itemd = None):
	if itemd == None:
		itemd = {}

	if path == None:
		path = []

	for node1 in node0:
		tag1 = scrub_tag(node1.tag)
		if node_ignored.get(tag1):
			continue

		value1 = (node1.text or "").strip()

		npath = list(path)
		npath.append(tag1)
		npathdot = ".".join(npath)

		if value1:
			itemd.setdefault(tag1, []).append(value1)
		else:
			d = node2d_raw(node1, npath)
			if d:
				itemd.setdefault(tag1, []).append(d)

	return	itemd

def node2d_simplify(node0, path = None, itemd = None):
	if itemd == None:
		itemd = {}

	if path == None:
		path = []

	for node1 in node0:
		tag1 = scrub_tag(node1.tag)
		if node_ignored.get(tag1):
			continue

		value1 = (node1.text or "").strip()

		npath = list(path)
		npath.append(tag1)
		npathdot = ".".join(npath)

		if value1:
			if node_listd.get(npathdot):
				itemd.setdefault(tag1, []).append(value1)
			else:
				itemd[tag1] = value1
		else:
			if node_flattend.get(npathdot):
				node2d_simplify(node1, npath, itemd = itemd)
			else:
				d = node2d_simplify(node1, npath)
				if d:
					itemd.setdefault(tag1, []).append(d)

	return	itemd

class AmazonECS:
	DEFAULT_LOCALE = "us"
	DEFAULT_ASSOCIATE = "onaswarm-20"
	API_VERSION = "2008-08-19" # http://docs.amazonwebservices.com/AWSECommerceService/2008-08-19/DG/

	## search index
	SI_All = "All"
	SI_Apparel = "Apparel"
	SI_Automotive = "Automotive"
	SI_Baby = "Baby"
	SI_Beauty = "Beauty"
	SI_Blended = "Blended"
	SI_Books = "Books"
	SI_Classical = "Classical"
	SI_DVD = "DVD"
	SI_DigitalMusic = "DigitalMusic"
	SI_Electronics = "Electronics"
	SI_GourmetFood = "GourmetFood"
	SI_HealthPersonalCare = "HealthPersonalCare"
	SI_HomeGarden = "HomeGarden"
	SI_Industrial = "Industrial"
	SI_Jewelry = "Jewelry"
	SI_KindleStore = "KindleStore"
	SI_Kitchen = "Kitchen"
	SI_MP3Downloads = "MP3Downloads"
	SI_Magazines = "Magazines"
	SI_Merchants = "Merchants"
	SI_Miscellaneous = "Miscellaneous"
	SI_Music = "Music"
	SI_MusicTracks = "MusicTracks"
	SI_MusicalInstruments = "MusicalInstruments"
	SI_OfficeProducts = "OfficeProducts"
	SI_OutdoorLiving = "OutdoorLiving"
	SI_PCHardware = "PCHardware"
	SI_PetSupplies = "PetSupplies"
	SI_Photo = "Photo"
	SI_Shoes = "Shoes"
	SI_SilverMerchants = "SilverMerchants"
	SI_Software = "Software"
	SI_SportingGoods = "SportingGoods"
	SI_Tools = "Tools"
	SI_Toys = "Toys"
	SI_UnboxVideo = "UnboxVideo"
	SI_VHS = "VHS"
	SI_Video = "Video"
	SI_VideoGames = "VideoGames"
	SI_Watches = "Watches"
	SI_Wireless = "Wireless"
	SI_WirelessAccessories  = "WirelessAccessories"

	## response group
	RG_Accessories = "Accessories"
	RG_BrowseNodes = "BrowseNodes"
	RG_EditorialReview = "EditorialReview"
	RG_Images = "Images"
	RG_ItemAttributes = "ItemAttributes"
	RG_ItemIds = "ItemIds"
	RG_Large = "Large"
	RG_ListmaniaLists = "ListmaniaLists"
	RG_Medium = "Medium"
	RG_MerchantItemAttributes = "MerchantItemAttributes"
	RG_OfferFull = "OfferFull"
	RG_OfferSummary = "OfferSummary"
	RG_Offers = "Offers"
	RG_Request = "Request"
	RG_Reviews = "Reviews"
	RG_SalesRank = "SalesRank"
	RG_SearchBins = "SearchBins"
	RG_Similarities = "Similarities"
	RG_Small = "Small"
	RG_Subjects = "Subjects"
	RG_Tracks = "Tracks"
	RG_VariationMinimum = "VariationMinimum"
	RG_VariationSummary = "VariationSummary"
	RG_Variations = "Variations"
	RG_DEFAULT = [ RG_Small, RG_Images, RG_Tracks ]

	## sorting  ... search dependent
	SORT_inverse_age_min = "-age-min"
	SORT_inverse_edition_sales_velocity = "-edition-sales-velocity"
	SORT_inverse_launch_date = "-launch-date"
	SORT_inverse_orig_rel_date = "-orig-rel-date"
	SORT_inverse_price = "-price"
	SORT_inverse_releasedate = "-releasedate"
	SORT_inverse_titlerank = "-titlerank"
	SORT_inverse_video_release_date = "-video-release-date"
	SORT_artistrank = "artistrank"
	SORT_daterank = "daterank"
	SORT_inverse_pricerank = "inverse-pricerank"
	SORT_inverseprice = "inverseprice"
	SORT_launch_date = "launch-date"
	SORT_orig_rel_date = "orig-rel-date"
	SORT_plus_pmrank = "+pmrank"
	SORT_pmrank = "pmrank"
	SORT_price = "price"
	SORT_pricerank = "pricerank"
	SORT_psrank = "psrank"
	SORT_release_date = "release-date"
	SORT_releasedate = "releasedate"
	SORT_relevancerank = "relevancerank"
	SORT_reviewrank = "reviewrank"
	SORT_sale_flag = "sale-flag"
	SORT_salesrank = "salesrank"
	SORT_songtitlerank = "songtitlerank"
	SORT_subslot_salesrank = "subslot-salesrank"
	SORT_titlerank = "titlerank"
	SORT_uploaddaterank = "uploaddaterank"
	SORT_xsrelevancerank = "xsrelevancerank"

	def __init__(self,
	  AccessKeyID, 
	  AssociateTag = DEFAULT_ASSOCIATE, 
	  Locale = DEFAULT_LOCALE, 
	  http_proxy = None, 
	  node2d = node2d_simplify,
	  max_pages = -1,
	):
		self.AccessKeyID = AccessKeyID
		self.Locale = Locale or AmazonECS.DEFAULT_LOCALE
		self.AssociateTag = AssociateTag or AmazonECS.DEFAULT_ASSOCIATE
		self.http_proxy = http_proxy
		self.max_pages = max_pages

		self.uri = None
		self.node2d = node2d

		if self.Locale:
			self.api_uri = Locale2api_urid.get(self.Locale.upper())

		if not self.AccessKeyID:
			raise	NoLicenseKeyError

		if not self.api_uri:
			raise	BadLocaleError, self.Locale

	def iter(self, **ad):
		return	self._iter(
			Operation = "ItemSearch", 
			Author = Author, 
			SearchIndex = SearchIndex,  
			**ad
		)

	def iterMusicByArtist(self, 
	  Artist, 
	  SearchIndex = SI_Music, 
	  ResponseGroup = RG_DEFAULT, 
	  Sort = SORT_relevancerank, 
	  **ad):
		return	self._iter(
			Operation = "ItemSearch", 
			Artist = Artist, 
			SearchIndex = SearchIndex,  
			ResponseGroup = ResponseGroup,
			Sort = Sort,
			**ad
		)

	def iterMusicByKeywords(self, 
	  Keywords, 
	  SearchIndex = SI_Music, 
	  ResponseGroup = RG_DEFAULT, 
	  Sort = SORT_relevancerank, 
	  **ad):
		return	self._iter(
			Operation = "ItemSearch", 
			Keywords = Keywords, 
			SearchIndex = SearchIndex,  
			ResponseGroup = ResponseGroup,
			Sort = Sort,
			**ad
		)

	def iterBooksByKeywords(self, 
	  Keywords, 
	  SearchIndex = SI_Books, 
	  ResponseGroup = RG_DEFAULT, 
	  Condition = "New",
	  Sort = SORT_relevancerank, 
	  **ad):
		"""
		Additional Parameters:
		- Format; Format = "CD" to get CDs, etc.
		"""

		return	self._iter(
			Operation = "ItemSearch", 
			Keywords = Keywords, 
			SearchIndex = SearchIndex,  
			ResponseGroup = ResponseGroup,
			Condition = "New",
			Sort = Sort,
			**ad
		)

	def iterBooksByAuthor(self, 
	  Keywords, 
	  SearchIndex = SI_Books, 
	  ResponseGroup = RG_DEFAULT, 
	  Condition = "New",
	  Sort = SORT_relevancerank, 
	  **ad):
		return	self._iter(
			Operation = "ItemSearch", 
			Author = Author, 
			SearchIndex = SearchIndex,  
			ResponseGroup = ResponseGroup,
			Condition = Condition,
			Sort = Sort,
			**ad
		)

	def _iter(self, **ad):
		TotalPages = None
		ItemPage = 0
		ItemCount = 0

		while TotalPages == None or ItemPage < TotalPages:
			ItemPage += 1
			if self.max_pages != -1 and ItemPage > self.max_pages:
				break

			#
			#	Read the data from the appropriate Amazon URI
			#
			url = self._makeURI(ItemPage = ItemPage, **ad)

			proxyd = None
			if self.http_proxy:
				proxyd = {
					"http" : self.http_proxy,
				}
			uopener = urllib.FancyURLopener(proxyd)

			try:
				uin = uopener.open(url)

				try:
					data = uin.read()
				finally:
					try: uin.close()
					except: pass
			except Exception, x:
				raise	NetworkError, x

			#
			#	Use element tree to convert to JSON-like object
			#
			try:
				tree = ElementTree.fromstring(data)
			except Exception, x:
				raise	BadResponseError, data

			items = None
			itemds = []

			for node in tree:
				tag = scrub_tag(node.tag)
				if tag == "Items":
					items = node
					break

			for node in items:
				tag = scrub_tag(node.tag)

				if tag == "TotalPages" and TotalPages == None:
					TotalPages = int(node.text)
				elif tag == "Item":
					itemd = self.node2d(node)
					if itemd:
						itemd['@Page'] = ItemPage
						itemd['@Index'] = ItemCount

						ItemCount += 1

						yield itemd

			if TotalPages == None:
				break

	def _makeURI(self, **ad):
		self.uri = self.api_uri
		self.uri += "&AWSAccessKeyId=%s" % self.AccessKeyID.strip()
		self.uri += "&Version=%s" % self.API_VERSION

		if self.AssociateTag:
			self.uri += "&AssociateTag=%s" % self.AssociateTag

		for key, value in ad.iteritems():
			if type(value) in types.StringTypes:
				self.uri += "&%s=%s" % ( key, urllib.quote(value), )
			elif type(value) == types.ListType:
				self.uri += "&%s=%s" % ( key, urllib.quote(",".join(value)), )
			else:
				self.uri += "&%s=%s" % ( key, value )

		return	self.uri


